#include "OpenGLFunctions.h"

// External variables
OGLSchene *_oglSchene;

// Private variables
int nbframes;
double lastTime;

void updateFPSCounter(void)
{
	double currTime;
	currTime = glfwGetTime();
	if(currTime - lastTime >= 2.0)
	{
		std::string windowTitle = _oglSchene->GetWindowName();
		windowTitle += IntToString(nbframes);
		glfwSetWindowTitle(windowTitle.c_str());

		nbframes = 0;
		lastTime = glfwGetTime();
	}
}

void scanKeyboardActions(void)
{
	if(glfwGetKey('R') == GLFW_PRESS)	// Reset Camera
	{
		_oglSchene->SetCamera();
		_oglSchene->SetScheneParams();

		return;
	}
	
	glm::vec3 camLoc, camDir, camOrient;
	GLfloat fov, aspectRatio, rangeMin, rangeMax;

	_oglSchene->GetCamera(&camLoc, &camDir, &camOrient);
	_oglSchene->GetScheneParams(&fov, &aspectRatio, &rangeMin, &rangeMax);

	if(glfwGetKey(GLFW_KEY_LEFT) == GLFW_PRESS)	// Move Camera Left
	{
		camLoc += glm::vec3(-1.0f, 0.0f, 0.0f);
		camDir += glm::vec3(-1.0f, 0.0f, 0.0f);
	}
	if(glfwGetKey(GLFW_KEY_RIGHT) == GLFW_PRESS)	// Move Camera Right
	{
		camLoc += glm::vec3(1.0f, 0.0f, 0.0f);
		camDir += glm::vec3(1.0f, 0.0f, 0.0f);
	}
	if(glfwGetKey(GLFW_KEY_UP) == GLFW_PRESS)		// Move Camera Up
	{
		camLoc += glm::vec3(0.0f, 1.0f, 0.0f);
		camDir += glm::vec3(0.0f, 1.0f, 0.0f);
	}
	if(glfwGetKey(GLFW_KEY_DOWN) == GLFW_PRESS)	// Move Camera Down
	{
		camLoc += glm::vec3(0.0f, -1.0f, 0.0f);
		camDir += glm::vec3(0.0f, -1.0f, 0.0f);
	}
	if(glfwGetKey('W') == GLFW_PRESS)
	{
		// Update View and/or Projection matrices
		float l = glm::length(glm::vec2(camLoc.y, camLoc.z));
		float a = asinf(camLoc.z / l);
		a -= 0.05f;
		float z = l * sin(a);
		float y = l * cos(a);
		camLoc = glm::vec3(camLoc.x, y, z);
	}
	if(glfwGetKey('A') == GLFW_PRESS)
	{
		// Update View and/or Projection matrices
		float l = glm::length(glm::vec2(camLoc.x, camLoc.z));
		float a = asinf(camLoc.z / l);
		a -= 0.05f;
		float z = l * sin(a);
		float x = l * cos(a);
		camLoc = glm::vec3(x, camLoc.y, z);
	}
	if(glfwGetKey('S') == GLFW_PRESS)
	{
		// Update View and/or Projection matrices
		float l = glm::length(glm::vec2(camLoc.y, camLoc.z));
		float a = asinf(camLoc.z / l);
		a += 0.05f;
		float z = l * sin(a);
		float y = l * cos(a);
		camLoc = glm::vec3(camLoc.x, y, z);
	}
	if(glfwGetKey('D') == GLFW_PRESS)
	{
		// Update View and/or Projection matrices
		float l = glm::length(glm::vec2(camLoc.x, camLoc.z));
		float a = asinf(camLoc.z / l);
		a += 0.05f;
		float z = l * sin(a);
		float x = l * cos(a);
		camLoc = glm::vec3(x, camLoc.y, z);
	}
	if(glfwGetKey(GLFW_KEY_KP_ADD) == GLFW_PRESS)	// Zoom in
	{
		camLoc += glm::vec3(0.0f, 0.0f, -1.0f);
	}
	if(glfwGetKey(GLFW_KEY_KP_SUBTRACT) == GLFW_PRESS)	// Zoom out
	{
		camLoc += glm::vec3(0.0f, 0.0f, 1.0f);
	}

	_oglSchene->SetCamera(camLoc, camDir, camOrient);
}

void scanMouseActions(void)
{
	// Nothing for now
}

void renderObjects(OGLDataObject *obj, GLuint mvpMatrixID)
{
	glm::mat4 pvmMat;

	glBindBuffer(obj->objType, obj->vboID);

	printf("Loading VBO : %u with drawType = %u and vertexCount = %i \n", 
		obj->vboID,
		obj->geomData->drawType,
		obj->geomData->vertexCount);

	// Enable Vertex Array Attributes
	std::vector<GLuint>::iterator it;
	for(it = obj->vaPointerIndex->begin(); it != obj->vaPointerIndex->end(); it++)
	{
		glEnableVertexAttribArray(*it);
		GLuint param;
		glGetVertexAttribIuiv(*it, GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING, &param);
		printf("Buffer bound to VAA %u is %u\n", *it, param);
	}
	pvmMat = _oglSchene->GetPVMat();
	pvmMat = pvmMat * obj->geomData->GetTSRMat();
	glUniformMatrix4fv(mvpMatrixID, 1, GL_FALSE, &pvmMat[0][0]);

	glDrawArrays(obj->geomData->drawType, 0, obj->geomData->vertexCount); // Draw Object

	// Disable Vertex Array Attributes
	for(it = obj->vaPointerIndex->begin(); it != obj->vaPointerIndex->end(); it++)
		glDisableVertexAttribArray(*it);

	//glBindBuffer(obj->objType, 0);
}

// This function binds the newly create Vertex Array Object so
// be sure to unbind/rebind other vaoids if you don't want to
// work with this one immediately.
int OGLCreateVertexArrayObject(GLuint *vaoid_out)
{
	GLenum errorCheckValue = glGetError();
	GLuint vaoid;
	glGenVertexArrays(1, &vaoid);
	glBindVertexArray(vaoid);

	errorCheckValue = glGetError();
	if (errorCheckValue != GL_NO_ERROR)
	{
		fprintf(
			stderr,
			"[ERROR] Could not create a VAO: %s \n",
			gluErrorString(errorCheckValue)
		);

		return OGL_ERROR;
	}

	*vaoid_out = vaoid;
	return OGL_SUCCESS;
}

// This function binds the newly create Vertex Buffer Object so
// be sure to unbind/rebind other vboids if you don't want to
// work with this one immediately.
int OGLCreateVertexBufferObject(
	GLvoid *vbd, 
	GLsizeiptr size, 
	GLenum data_type, 
	GLenum data_usage, 
	GLuint *vboid_out)
{
	GLenum errorCheckValue = glGetError();
	GLuint vboid;
	glGenBuffers(1, &vboid); // Identifies 1 vertex buffer
	glBindBuffer(data_type, vboid);	// Now bind the buffer for further function calls

	// Give OpenGl our new vertex information
	glBufferData(data_type, size, vbd, data_usage);
	
	errorCheckValue = glGetError();
	if (errorCheckValue != GL_NO_ERROR)
	{
		fprintf(
			stderr,
			"[ERROR] Could not create a VBO: %s \n",
			gluErrorString(errorCheckValue)
		);

		return OGL_ERROR;
	}

	*vboid_out = vboid;
	return OGL_SUCCESS;
}

// Taken from : http://www.opengl-tutorial.org/beginners-tutorials/tutorial-2-the-first-triangle/
GLuint OGLLoadShader(const char *shader_file_path, const int shader_type)
{ 
    // Create the shaders
    GLuint shaderID = glCreateShader(shader_type);
 
    // Read the Shader code from the file
    std::string shaderCode;
    std::ifstream shaderStream(shader_file_path, std::ios::in);
    if(shaderStream.is_open())
    {
        std::string line = "";
        while(std::getline(shaderStream, line))
            shaderCode += "\n" + line;
        shaderStream.close();
    }
 
    GLint result = GL_FALSE;
    int infoLogLength;
 
    // Compile Shader
    printf("Compiling shader : %s\n", shader_file_path);
    char const *sourcePointer = shaderCode.c_str();
    glShaderSource(shaderID, 1, &sourcePointer , NULL);
    glCompileShader(shaderID);
 
    // Check Shader
    glGetShaderiv(shaderID, GL_COMPILE_STATUS, &result);
    glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &infoLogLength);
    std::vector<char> shaderErrorMessage(infoLogLength);
    glGetShaderInfoLog(shaderID, infoLogLength, NULL, &shaderErrorMessage[0]);
    fprintf(stdout, "%s\n", &shaderErrorMessage[0]);
 
	return shaderID;
}

// Taken from : http://www.opengl-tutorial.org/beginners-tutorials/tutorial-2-the-first-triangle/
// The program is created AND LINKED in this function. To use another program immediately
// after calling this function, unlink this one first.
GLuint OGLLoadProgram(std::vector<GLuint> shader_ids)
{
	// Link the program
    fprintf(stdout, "Linking program\n");
    GLuint programID = glCreateProgram();
	std::vector<GLuint>::iterator it;
	for(it = shader_ids.begin(); it < shader_ids.end(); it++)
	{
		glAttachShader(programID, *it);
	}
    glLinkProgram(programID);
 
    // Check the program
	GLint result = GL_FALSE;
    int infoLogLength;
    glGetProgramiv(programID, GL_LINK_STATUS, &result);
    glGetProgramiv(programID, GL_INFO_LOG_LENGTH, &infoLogLength);
    std::vector<char> programErrorMessage( std::max(infoLogLength, int(1)) );
    glGetProgramInfoLog(programID, infoLogLength, NULL, &programErrorMessage[0]);
    fprintf(stdout, "%s\n", &programErrorMessage[0]);
 
    return programID;
}

void OGLMainLoop()
{
	// Inintialize Main Loop Variables
	nbframes = 0;
	lastTime = glfwGetTime();
	
	GLuint matrixID;
	_oglSchene->GetMatrixID("MVP", &matrixID);
	glm::mat4 pvmMat;

	// Load the Shader Program to use
	GLuint programID;
	_oglSchene->GetProgramID("GLSLProgram", &programID);
	glUseProgram(programID);

	// Main Loop Start
	while((glfwGetKey(GLFW_KEY_ESC) != GLFW_PRESS) 
		&& glfwGetWindowParam(GLFW_OPENED))
	{
		// Update FPS Counter
		updateFPSCounter();

		// I) Render Floor
		// Bind the correct vertex array
		GLuint vaoID;
		_oglSchene->GetVAOID("VAO", &vaoID);
		glBindVertexArray(vaoID);
		
		// Clear Buffers
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		// Check for keystrokes & mouse actions
		scanMouseActions();
		scanKeyboardActions();

		// if OGLDataObject *is an array, count is the size of the array
		int count;
		OGLDataObject *obj = NULL;
		std::string vboName = "VBOn";

		// Render Objects
		_oglSchene->GetObjectBufferData("VBOFloor", &obj, &count);
		renderObjects(obj, matrixID);

		// II) Render Droplets
		for(int i = 0; i < NUM_ROBOTS; i++)
		{
			_oglSchene->GetObjectBufferData(vboName.replace(3, 1, IntToString(i+1)), &obj, &count);
			renderObjects(obj, matrixID);
		}

		// Swap Display Buffers
		glfwSwapBuffers();	// Implicitely calls glfwPollEvents() after it is finished.

		// Update Main loop variables
		nbframes++;		
	
	}	// Main Loop End
}

void GLFWCALL OGLWindowResizeCallback(int width, int height)
{
	_oglSchene->UpdateWindowDims(width, height, false);
}

void GLFWCALL OGLKeystrokeCallback(int ch, int action)
{
	// Nothing for now
}

void GLFWCALL OGLMouseButtonCallback(int button, int action)
{
	// Nothing for now
}

std::string IntToString(int number)
{
	std::stringstream ss;//create a stringstream
	ss << number;//add number to the stream
	return ss.str();//return a string with the contents of the stream
}